﻿using System;
using System.Collections.Generic;
using System.Text;
using RouterControl.LocalDb;
using RouterControl.IO;
using System.Threading;
using System.Linq;
using RouterControl.Whs;

namespace RouterControl.Service.DeviceSlaves
{
    [RouterSlave]
    public sealed class TrafficStatsSlave : RouterSlave
    {
        public enum TrafficMode
        {
            Unknown,
            Delta,
            Total
        }

        public struct StatsCacheEntry
        {
            public uint UploadedBytes;
            public uint DownloadedBytes;

            public static readonly StatsCacheEntry MinValue = new StatsCacheEntry()
            {
                UploadedBytes = uint.MinValue,
                DownloadedBytes = uint.MinValue,
            };

            public static readonly StatsCacheEntry MaxValue = new StatsCacheEntry()
            {
                UploadedBytes = uint.MaxValue,
                DownloadedBytes = uint.MaxValue,
            };
        }

        private List<StatsCacheEntry> _statsCache;
        private LocalDbConnector _localDb;
        private LocalDbConnector.DeviceInfo? _localDbDevice;
        private bool _isTrafficSupported = true;
        private TrafficMode _trafficMode = TrafficMode.Unknown;
        private Timer _gatherTimer;

        /* we use our own timer here
        public override void OnDataUpdated()
        {
            base.OnDataUpdated();

            // continue
            try
            {
                GatherStats();
            }
            catch (Exception x)
            {
                _log.Warn("Failed to gather traffic stats", x);

                throw;
            }
        }*/

        private void GatherStats()
        {
            // pre-check
            if (!_isTrafficSupported || 
                (this.Router == null) || 
                !Properties.Settings.Default.EnableTrafficStats)
            {
                return;
            }

            // update traffic data
            var router = this.Router;
            var localDbDevice = _localDbDevice;
            var data = router.Summary;

            // traffic supported?
            if (data.UploadRateBytesPerSecond.HasValue && data.DownloadRateBytesPerSecond.HasValue)
            {
                // add to stats
                var ne = new StatsCacheEntry()
                {
                    UploadedBytes = data.UploadRateBytesPerSecond.Value,
                    DownloadedBytes = data.DownloadRateBytesPerSecond.Value,
                };

                _statsCache.Add(ne);

                // push cache to local db
                var length = Properties.Settings.Default.TrafficStatsLength;

                if (_statsCache.Count > length)
                {
                    // determine traffic data mode
                    if (_trafficMode == TrafficMode.Unknown)
                        DetermineTrafficMode();
                    
                    // handle traffic data
                    // if mode is known
                    if (_trafficMode != TrafficMode.Unknown)
                    {
                        var trafficValue = GetAverageFromCache();

                        _localDb.AddTrafficInfo(
                            localDbDevice.Value,
                            new LocalDbConnector.TrafficInfo()
                            {
                                TimeStamp = DateTime.Now,
                                UpBytesPerSecond = (int)trafficValue.UploadedBytes,
                                DownBytesPerSecond = (int)trafficValue.DownloadedBytes
                            });
                    }

                    // reset cache
                    _statsCache.Clear();
                }
            }
            else
            {
                // not supported; stop checking.
                _isTrafficSupported = false;
                _localDbDevice = null;
            }
        }

        private void DetermineTrafficMode()
        {
            _trafficMode = TrafficMode.Unknown;

            // persist cache
            var cache = _statsCache.ToArray();

            if (cache.Length <= 2)
            {
                _log.Warn("Traffic Mode cannot be determined: cache does not contain enough entries: " + cache.Length);

                return;
            }

            // determine mode
            StatsCacheEntry? last = null;
            var deltaIndicator = 0;

            foreach (var e in cache)
            {
                if (last.HasValue)
                {
                    if ((e.DownloadedBytes < last.Value.DownloadedBytes) ||
                        (e.UploadedBytes < last.Value.UploadedBytes) ||
                        (e.UploadedBytes <= 0) || (e.DownloadedBytes <=0))
                    {
                        deltaIndicator++;
                    }
                }

                // next value
                last = e;
            }

            // do we have enough information?
            var totalTraffic = (
                from c in cache
                select (long)c.DownloadedBytes + c.UploadedBytes
                ).Sum();

            if (totalTraffic < 1024)
            {
                // the information is not enough to rely on
                _log.Warn("Traffic Mode cannot be determined: not enough traffic to rely on");

                return;
            }

            // finally determine mode
            // if the traffic was at least ? times smaller than
            // the previous value or 0 then we have a delta value
            //
            // note: one time smaller is possible because of overflow
            //       (event two times but that's very unlikely)
            _trafficMode = (deltaIndicator >= 4 ? TrafficMode.Delta : TrafficMode.Total);

            _log.Info("Traffic Mode: " + _trafficMode);
        }

        public override void OnRouterChanged(CommonInternetGateway newRouter)
        {
            base.OnRouterChanged(newRouter);

            // continue
            _isTrafficSupported = true;
            _trafficMode = TrafficMode.Unknown;
            _statsCache.Clear();

            if (newRouter != null)
            {
                _localDbDevice = _localDb.EnsureDevice(
                    newRouter.Device.UniqueDeviceName,
                    newRouter.Device.Type
                    );
            }
        }

        public StatsCacheEntry GetAverageFromCache()
        {
            switch (_trafficMode)
            {
                case TrafficMode.Unknown:
                    throw new InvalidOperationException("traffic mode not determined");

                case TrafficMode.Delta:
                {
                    // persist cache
                    var cache = _statsCache.ToArray();

                    if (cache.Length <= 0)
                        return StatsCacheEntry.MinValue;

                    // calculate
                    var ret = StatsCacheEntry.MinValue;

                    foreach (var v in cache)
                    {
                        ret.UploadedBytes += v.UploadedBytes;
                        ret.DownloadedBytes += v.DownloadedBytes;
                    }

                    ret.UploadedBytes /= (uint)cache.Length;
                    ret.DownloadedBytes /= (uint)cache.Length;

                    return ret;
                }

                case TrafficMode.Total:
                {
                    // persist cache
                    var cache = _statsCache.ToArray();

                    if (cache.Length <= 0)
                        return StatsCacheEntry.MinValue;

                    // calculate
                    var first = cache[0];
                    var last = cache[cache.Length - 1];

                    // handle overflow
                    if (first.DownloadedBytes > last.DownloadedBytes)
                    {
                        last.DownloadedBytes += (uint.MaxValue - first.DownloadedBytes);
                        first.DownloadedBytes = 0;
                    }

                    if (first.UploadedBytes > last.UploadedBytes)
                    {
                        last.UploadedBytes += (uint.MaxValue - first.UploadedBytes);
                        first.UploadedBytes = 0;
                    }

                    // finalize
                    return new StatsCacheEntry()
                    {
                        DownloadedBytes = ((last.DownloadedBytes - first.DownloadedBytes) / (uint)cache.Length),
                        UploadedBytes = ((last.UploadedBytes - first.UploadedBytes) / (uint)cache.Length)
                    };
                }

                default:
                    throw new Exception("unknown traffic mode: " + _trafficMode);
            }
        }

        private void OnTick(object state)
        {
            try
            {
                var router = this.Router;

                if (router != null)
                {
                    router.ClearCacheTrafficData();

                    GatherStats();
                }
            }
            catch (DeviceConnectionLostException x)
            {
                _log.Warn("Device connection lost; stop gathering traffic", x);

                _isTrafficSupported = false;
            }
            catch (Exception x)
            {
                _log.Error("Failed to gather traffic stats", x);

                // do not pass error to client
            }
        }

        public TrafficStatsSlave()
        {
            _statsCache = new List<StatsCacheEntry>(Properties.Settings.Default.TrafficStatsLength + 1);

            _localDb = new LocalDbConnector();

            // create timer
            _gatherTimer = new Timer(
                new TimerCallback(OnTick),
                null,
                1000,
                Properties.Settings.Default.TrafficStatsIntervalMs
                );
        }
    }
}
